package io.elegant.sessions

import android.content.Context
import com.google.protobuf.Any
import io.elegant.*
import io.elegant.Elegant.Interaction
import io.elegant.ElegantApiGrpc.ElegantApiStub
import kotlinx.coroutines.launch
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.reflect.KClass

abstract class BaseSession(
    private val stub: ElegantApiStub,
    private val context: Context
): AbstractSession() {
    private var id: Long = 0
    var onDestroyedListener: (() -> Unit)? = null
    private var parent: BaseSession? = null
    private val sessions = mutableSetOf<BaseSession>()
    private val resSet = mutableSetOf<Continuation<Any>>()
    override suspend fun onDestroy(throwable: Throwable?) {
        if (throwable == null) {
            onDestroy()
            return
        }
        onReset ()
    }
    override suspend fun onReceive(name: String, sessionId: Long, futureId: Long, vararg argv: Any) {
        when(name) {
            "ready" -> ready(*argv)
            "create" -> create(sessionId, futureId, *argv)
            "destroy" -> destroy()
            "callbackResult" -> callbackResult(*argv)
        }
    }
    override suspend fun onError(throwable: Throwable?, sessionId: Long, futureId: Long) = sendAction("onError", sessionId, futureId, throwable?.message?.toAny() ?: "An unknown error.".toAny())
    fun sendAction(name: String, sessionId: Long = 0, futureId: Long = 0, vararg argv: Any) = try {
        Interaction
            .newBuilder()
            .setName(name)
            .setSession(sessionId)
            .setFuture(futureId)
            .addAllArgv(argv.toList())
            .build()
            .let (this::send)
    } catch (e: IllegalAccessException) {
        e.printStackTrace()
    }
    suspend fun sendActionUntilCallbackResult(name: String, sessionId: Long = 0, futureId: Long = 0, vararg argv: Any) = suspendCoroutine {
        resSet.add(it)
        sendAction(name, sessionId, futureId, argv = arrayOf(it.hashCode().toAny()) + argv)
    }
    open fun onCreate(sessionId: Long = 0, futureId: Long = 0, vararg argv: Any) = sendAction("onCreate", sessionId, futureId, *argv)
    open fun onDestroy() {
        sessions.toTypedArray().forEach {
            it.onDestroy()
        }
        sessions.clear()
        resSet.clear()
        try {
            sendAction("onDestroy")
        } catch (e: IllegalStateException) {
            e.printStackTrace()
        } catch (e: NullPointerException) {
            e.printStackTrace()
        }
        onDestroyedListener?.let { it () }
        onDestroyedListener = null
    }
    open fun onReset() = onDestroy()
    override fun create(sessionId: Long, futureId: Long, vararg argv: Any) {
        if (argv.isEmpty())
            throw IllegalArgumentException("Missing param `clsName` from the server calling this `create` method.")
        val clsName = argv[0].toStringValue()
        clsName
            .toSessionClass()
            ?.newSession(sessionId, futureId, *argv.slice(1 until argv.size).toTypedArray())
            ?: throw IllegalArgumentException("The param `clsName` (value is ${clsName}) is invalid from the server calling this `create` method.")
    }
    override fun destroy() {
        onDestroy()
        super.destroy()
    }
    private fun KClass<out BaseSession>.newSession(sessionId: Long = 0, futureId: Long = 0, vararg argv: Any) {
        val session = constructors.first().call(stub, context)
        session.parent = this@BaseSession
        session.onDestroyedListener = {
            sessions.remove(session)
            session.parent = null
        }
        sessions.add(session)
        session.onCreate(sessionId, futureId, *argv)
    }
    protected fun <T: BaseSession> Any.getReference(): T? {
        val id = this.toLongValue()
        val session = sessions.find { it.id == id }
        @Suppress("UNCHECKED_CAST")
        if (session != null)
            return session as T
        return parent?.run {
            this@getReference.getReference()
        }
    }
    fun Throwable.sendError (sessionId: Long = 0, futureId: Long = 0) = csIo.launch {
        onError(this@sendError, sessionId, futureId)
    }
    suspend fun Any.sendCallback(vararg argv: Any) = suspendCoroutine { cont ->
        resSet.add(cont)
        toInteraction()
            ?.let {
                sendAction("onCallback", it.session, it.future, argv = arrayOf(cont.hashCode().toAny()) + argv)
            }
    }
    private fun ready(vararg argv: Any) {
        this.id = argv[0].toLongValue()
    }
    private fun callbackResult(vararg argv: Any) = resSet.find {
        it.hashCode() == argv[0].toIntValue()
    }?.let {
        resSet.remove(it)
        it.resume(argv[1])
    }
}